/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of Quake III Arena source code.

Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
 */

#include "server.h"

#ifdef USE_RUBY
#include "ruby.h"
#endif
/*
===============================================================================

OPERATOR CONSOLE ONLY COMMANDS

These commands can only be entered from stdin or by a remote operator datagram
===============================================================================
 */

/*
==================
SV_GetPlayerByHandle

Returns the player with player id or name from Cmd_Argv(1)
==================
 */
static client_t *SV_GetPlayerByHandle(void) {
    client_t *cl;
    int i;
    char *s;
    char cleanName[64];

    // make sure server is running
    if (!com_sv_running->integer) {
        return NULL;
    }

    if (Cmd_Argc() < 2) {
        Com_Printf("No player specified.\n");
        return NULL;
    }

    s = Cmd_Argv(1);

    // Check whether this is a numeric player handle
    for (i = 0; s[i] >= '0' && s[i] <= '9'; i++);

    if (!s[i]) {
        int plid = atoi(s);

        // Check for numeric playerid match
        if (plid >= 0 && plid < sv_maxclients->integer) {
            cl = &svs.clients[plid];

            if (cl->state)
                return cl;
        }
    }

    // check for a name match
    for (i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++) {
        if (!cl->state) {
            continue;
        }
        if (!Q_stricmp(cl->name, s)) {
            return cl;
        }

        Q_strncpyz(cleanName, cl->name, sizeof (cleanName));
        Q_CleanStr(cleanName);
        if (!Q_stricmp(cleanName, s)) {
            return cl;
        }
    }

    Com_Printf("Player %s is not on the server\n", s);

    return NULL;
}

/*
==================
SV_GetPlayerByNum

Returns the player with idnum from Cmd_Argv(1)
==================
 */
static client_t *SV_GetPlayerByNum(void) {
    client_t *cl;
    int i;
    int idnum;
    char *s;

    // make sure server is running
    if (!com_sv_running->integer) {
        return NULL;
    }

    if (Cmd_Argc() < 2) {
        Com_Printf("No player specified.\n");
        return NULL;
    }

    s = Cmd_Argv(1);

    for (i = 0; s[i]; i++) {
        if (s[i] < '0' || s[i] > '9') {
            Com_Printf("Bad slot number: %s\n", s);
            return NULL;
        }
    }
    idnum = atoi(s);
    if (idnum < 0 || idnum >= sv_maxclients->integer) {
        Com_Printf("Bad client slot: %i\n", idnum);
        return NULL;
    }

    cl = &svs.clients[idnum];
    if (!cl->state) {
        Com_Printf("Client %i is not active\n", idnum);
        return NULL;
    }
    return cl;
}

//=========================================================

/*
==================
SV_Map_f

Restart the server on a different map
==================
 */
static void SV_Map_f(void) {
    char *cmd;
    char *map;
    qboolean killBots, cheat, omitted;
    char expanded[MAX_QPATH];
    char mapname[MAX_QPATH];

    omitted = qfalse;

    map = Cmd_Argv(1);
    if (!map) {
        return;
    }

    // make sure the level exists before trying to change, so that
    // a typo at the server console won't end the game
    Com_sprintf(expanded, sizeof (expanded), "maps/%s.bsp", map);
    if (FS_ReadFile(expanded, NULL) == -1) {
        Com_sprintf(expanded, sizeof (expanded), "maps/r_%s.bsp", map);
        if (FS_ReadFile(expanded, NULL) == -1) {
            Com_Printf("Can't find map %s\n", expanded);
            return;
        }
        omitted = qtrue;
    }

    // force latched values to get set
    Cvar_Get("g_gametype", "0", CVAR_SERVERINFO | CVAR_USERINFO | CVAR_LATCH);

    cmd = Cmd_Argv(0);
    if (Q_stricmpn(cmd, "sp", 2) == 0) {
        Cvar_SetValue("g_gametype", GT_SINGLE_PLAYER);
        Cvar_SetValue("g_doWarmup", 0);
        // may not set sv_maxclients directly, always set latched
        Cvar_SetLatched("sv_maxclients", "8");
        cmd += 2;
        cheat = qfalse;
        killBots = qtrue;
    } else {
        if (!Q_stricmp(cmd, "devmap") || !Q_stricmp(cmd, "spdevmap")) {
            cheat = qtrue;
            killBots = qtrue;
        } else {
            cheat = qfalse;
            killBots = qfalse;
        }
        if (sv_gametype->integer == GT_SINGLE_PLAYER) {
            Cvar_SetValue("g_gametype", GT_FFA);
        }
    }

    // save the map name here cause on a map restart we reload the q3config.cfg
    // and thus nuke the arguments of the map command
    if (omitted == qtrue) {
        Q_strncpyz(mapname, va("r_%s", map), sizeof (mapname));
    } else {
        Q_strncpyz(mapname, map, sizeof (mapname));
    }

    // start up the map
    SV_SpawnServer(mapname, killBots);

    // set the cheat value
    // if the level was started with "map <levelname>", then
    // cheats will not be allowed.  If started with "devmap <levelname>"
    // then cheats will be allowed
    if (cheat) {
        Cvar_Set("sv_cheats", "1");
    } else {
        Cvar_Set("sv_cheats", "0");
    }
}

/*
================
SV_MapRestart_f

Completely restarts a level, but doesn't send a new gamestate to the clients.
This allows fair starts with variable load times.
================
 */
static void SV_MapRestart_f(void) {
    int i;
    client_t *client;
    char *denied;
    qboolean isBot;
    int delay;

    // make sure we aren't restarting twice in the same frame
    if (com_frameTime == sv.serverId) {
        return;
    }

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (sv.restartTime) {
        return;
    }

    if (Cmd_Argc() > 1) {
        delay = atoi(Cmd_Argv(1));
    } else {
        delay = 5;
    }
    if (delay && !Cvar_VariableValue("g_doWarmup")) {
        sv.restartTime = sv.time + delay * 1000;
        SV_SetConfigstring(CS_WARMUP, va("%i", sv.restartTime));
        return;
    }

    // check for changes in variables that can't just be restarted
    // check for maxclients change
    if (sv_maxclients->modified || sv_gametype->modified) {
        char mapname[MAX_QPATH];

        Com_Printf("variable change -- restarting.\n");
        // restart the map the slow way
        Q_strncpyz(mapname, Cvar_VariableString("mapname"), sizeof ( mapname));

        SV_SpawnServer(mapname, qfalse);
        return;
    }

    // toggle the server bit so clients can detect that a
    // map_restart has happened
    svs.snapFlagServerBit ^= SNAPFLAG_SERVERCOUNT;

    // generate a new serverid
    // TTimo - don't update restartedserverId there, otherwise we won't deal correctly with multiple map_restart
    sv.serverId = com_frameTime;
    Cvar_Set("sv_serverid", va("%i", sv.serverId));

    // if a map_restart occurs while a client is changing maps, we need
    // to give them the correct time so that when they finish loading
    // they don't violate the backwards time check in cl_cgame.c
    for (i = 0; i < sv_maxclients->integer; i++) {
        if (svs.clients[i].state == CS_PRIMED) {
            svs.clients[i].oldServerTime = sv.restartTime;
        }
    }

    // reset all the vm data in place without changing memory allocation
    // note that we do NOT set sv.state = SS_LOADING, so configstrings that
    // had been changed from their default values will generate broadcast updates
    sv.state = SS_LOADING;
    sv.restarting = qtrue;

    SV_RestartGameProgs();

    // run a few frames to allow everything to settle
    for (i = 0; i < 3; i++) {
        VM_Call(gvm, GAME_RUN_FRAME, sv.time);
        sv.time += 100;
        svs.time += 100;
    }

    sv.state = SS_GAME;
    sv.restarting = qfalse;

    // connect and begin all the clients
    for (i = 0; i < sv_maxclients->integer; i++) {
        client = &svs.clients[i];

        // send the new gamestate to all connected clients
        if (client->state < CS_CONNECTED) {
            continue;
        }

        if (client->netchan.remoteAddress.type == NA_BOT) {
            isBot = qtrue;
        } else {
            isBot = qfalse;
        }

        // add the map_restart command
        SV_AddServerCommand(client, "map_restart\n");

        // connect the client again, without the firstTime flag
        denied = VM_ExplicitArgPtr(gvm, VM_Call(gvm, GAME_CLIENT_CONNECT, i, qfalse, isBot));
        if (denied) {
            // this generally shouldn't happen, because the client
            // was connected before the level change
            SV_DropClient(client, denied);
            Com_Printf("SV_MapRestart_f(%d): dropped client %i - denied!\n", delay, i);
            continue;
        }

        client->state = CS_ACTIVE;

        SV_ClientEnterWorld(client, &client->lastUsercmd);
    }

    // run another frame to allow things to look at all the players
    VM_Call(gvm, GAME_RUN_FRAME, sv.time);
    sv.time += 100;
    svs.time += 100;
}

//===============================================================

/*
==================
SV_Kick_f

Kick a user off of the server  FIXME: move to game
==================
 */
static void SV_Kick_f(void) {
    client_t *cl;
    int i;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() <= 1 || Cmd_Argc() > 3) {
        Com_Printf("Usage: kick <player> [reason]\nkick all [reason] = kick everyone\nkick allbots = kick all bots\n");
        return;
    }

    cl = SV_GetPlayerByHandle();
    if (!cl) {
        if (!Q_stricmp(Cmd_Argv(1), "all")) {
            for (i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++) {
                if (!cl->state) {
                    continue;
                }
                if (cl->netchan.remoteAddress.type == NA_LOOPBACK) {
                    continue;
                }
                if (Cmd_Argv(2) && Q_stricmp(Cmd_Argv(2), "") && Q_stricmp(Cmd_Argv(2), " ")) {
                    SV_DropClient(cl, va("was kicked (%s)", Cmd_Argv(2)));
                } else {
                    SV_DropClient(cl, "was kicked");
                }
                cl->lastPacketTime = svs.time; // in case there is a funny zombie
            }
        } else if (!Q_stricmp(Cmd_Argv(1), "allbots")) {
            for (i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++) {
                if (!cl->state) {
                    continue;
                }
                if (cl->netchan.remoteAddress.type != NA_BOT) {
                    continue;
                }
                SV_DropClient(cl, "was kicked");
                cl->lastPacketTime = svs.time; // in case there is a funny zombie
            }
        }
        return;
    }
    if (cl->netchan.remoteAddress.type == NA_LOOPBACK) {
        SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
        return;
    }

    if (Cmd_Argv(2) && Q_stricmp(Cmd_Argv(2), "") && Q_stricmp(Cmd_Argv(2), " ")) {
        SV_DropClient(cl, va("was kicked (%s)", Cmd_Argv(2)));
    } else {
        SV_DropClient(cl, "was kicked");
    }
    cl->lastPacketTime = svs.time; // in case there is a funny zombie
}

#ifndef STANDALONE
// these functions require the auth server which of course is not available anymore for stand-alone games.

/*
==================
SV_Ban_f

Ban a user from being able to play on this server through the auth
server
==================
 */
static void SV_Ban_f(void) {
    client_t *cl;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() != 2) {
        Com_Printf("Usage: banUser <player name>\n");
        return;
    }

    cl = SV_GetPlayerByHandle();

    if (!cl) {
        return;
    }

    if (cl->netchan.remoteAddress.type == NA_LOOPBACK) {
        SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
        return;
    }

    // look up the authorize server's IP
    /*if (!svs.authorizeAddress.ip[0] && svs.authorizeAddress.type != NA_BAD) {
        Com_Printf("Resolving %s\n", AUTHORIZE_SERVER_NAME);
        if (!NET_StringToAdr(AUTHORIZE_SERVER_NAME, &svs.authorizeAddress, NA_IP)) {
            Com_Printf("Couldn't resolve address\n");
            return;
        }
        svs.authorizeAddress.port = BigShort(PORT_AUTHORIZE);
        Com_Printf("%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME,
                svs.authorizeAddress.ip[0], svs.authorizeAddress.ip[1],
                svs.authorizeAddress.ip[2], svs.authorizeAddress.ip[3],
                BigShort(svs.authorizeAddress.port));
    }

    // otherwise send their ip to the authorize server
    if (svs.authorizeAddress.type != NA_BAD) {
        NET_OutOfBandPrint(NS_SERVER, svs.authorizeAddress,
                "banUser %i.%i.%i.%i", cl->netchan.remoteAddress.ip[0], cl->netchan.remoteAddress.ip[1],
                cl->netchan.remoteAddress.ip[2], cl->netchan.remoteAddress.ip[3]);
        Com_Printf("%s was banned from coming back\n", cl->name);
    }*/
}

/*
==================
SV_BanNum_f

Ban a user from being able to play on this server through the auth
server
==================
 */
static void SV_BanNum_f(void) {
    client_t *cl;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() != 2) {
        Com_Printf("Usage: banClient <client number>\n");
        return;
    }

    cl = SV_GetPlayerByNum();
    if (!cl) {
        return;
    }
    if (cl->netchan.remoteAddress.type == NA_LOOPBACK) {
        SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
        return;
    }

    // look up the authorize server's IP
    /*if (!svs.authorizeAddress.ip[0] && svs.authorizeAddress.type != NA_BAD) {
        Com_Printf("Resolving %s\n", AUTHORIZE_SERVER_NAME);
        if (!NET_StringToAdr(AUTHORIZE_SERVER_NAME, &svs.authorizeAddress, NA_IP)) {
            Com_Printf("Couldn't resolve address\n");
            return;
        }
        svs.authorizeAddress.port = BigShort(PORT_AUTHORIZE);
        Com_Printf("%s resolved to %i.%i.%i.%i:%i\n", AUTHORIZE_SERVER_NAME,
                svs.authorizeAddress.ip[0], svs.authorizeAddress.ip[1],
                svs.authorizeAddress.ip[2], svs.authorizeAddress.ip[3],
                BigShort(svs.authorizeAddress.port));
    }

    // otherwise send their ip to the authorize server
    if (svs.authorizeAddress.type != NA_BAD) {
        NET_OutOfBandPrint(NS_SERVER, svs.authorizeAddress,
                "banUser %i.%i.%i.%i", cl->netchan.remoteAddress.ip[0], cl->netchan.remoteAddress.ip[1],
                cl->netchan.remoteAddress.ip[2], cl->netchan.remoteAddress.ip[3]);
        Com_Printf("%s was banned from coming back\n", cl->name);
    }*/
}
#endif

/*
==================
SV_RehashBans_f

Load saved bans from file.
==================
 */
static void SV_RehashBans_f(void) {
    int index, filelen;
    fileHandle_t readfrom;
    char *textbuf, *curpos, *maskpos, *newlinepos, *endpos;
    char filepath[MAX_QPATH];

    serverBansCount = 0;

    if (!sv_banFile->string || !*sv_banFile->string)
        return;

    if (!(curpos = Cvar_VariableString("fs_game")) || !*curpos)
        curpos = BASEGAME;

    Com_sprintf(filepath, sizeof (filepath), "%s/%s", curpos, sv_banFile->string);

    if ((filelen = FS_SV_FOpenFileRead(filepath, &readfrom)) >= 0) {
        if (filelen < 2) {
            // Don't bother if file is too short.
            FS_FCloseFile(readfrom);
            return;
        }

        curpos = textbuf = Z_Malloc(filelen);

        filelen = FS_Read(textbuf, filelen, readfrom);
        FS_FCloseFile(readfrom);

        endpos = textbuf + filelen;

        for (index = 0; index < SERVER_MAXBANS && curpos + 2 < endpos; index++) {
            // find the end of the address string
            for (maskpos = curpos + 2; maskpos < endpos && *maskpos != ' '; maskpos++);

            if (maskpos + 1 >= endpos)
                break;

            *maskpos = '\0';
            maskpos++;

            // find the end of the subnet specifier
            for (newlinepos = maskpos; newlinepos < endpos && *newlinepos != '\n'; newlinepos++);

            if (newlinepos >= endpos)
                break;

            *newlinepos = '\0';

            if (NET_StringToAdr(curpos + 2, &serverBans[index].ip, NA_UNSPEC)) {
                serverBans[index].isexception = (curpos[0] != '0');
                serverBans[index].subnet = atoi(maskpos);

                if (serverBans[index].ip.type == NA_IP &&
                        (serverBans[index].subnet < 1 || serverBans[index].subnet > 32)) {
                    serverBans[index].subnet = 32;
                } else if (serverBans[index].ip.type == NA_IP6 &&
                        (serverBans[index].subnet < 1 || serverBans[index].subnet > 128)) {
                    serverBans[index].subnet = 128;
                }
            }

            curpos = newlinepos + 1;
        }

        serverBansCount = index;

        Z_Free(textbuf);
    }
}

/*
==================
SV_WriteBans_f

Save bans to file.
==================
 */
static void SV_WriteBans(void) {
    int index;
    fileHandle_t writeto;
    char *curpos, filepath[MAX_QPATH];

    if (!sv_banFile->string || !*sv_banFile->string)
        return;

    if (!(curpos = Cvar_VariableString("fs_game")) || !*curpos)
        curpos = BASEGAME;

    Com_sprintf(filepath, sizeof (filepath), "%s/%s", curpos, sv_banFile->string);

    if ((writeto = FS_SV_FOpenFileWrite(filepath))) {
        char writebuf[128];
        serverBan_t *curban;

        for (index = 0; index < serverBansCount; index++) {
            curban = &serverBans[index];

            Com_sprintf(writebuf, sizeof (writebuf), "%d %s %d\n",
                    curban->isexception, NET_AdrToString(curban->ip), curban->subnet);
            FS_Write(writebuf, strlen(writebuf), writeto);
        }

        FS_FCloseFile(writeto);
    }
}

/*
==================
SV_DelBanEntryFromList

Remove a ban or an exception from the list.
==================
 */

static qboolean SV_DelBanEntryFromList(int index) {
    if (index == serverBansCount - 1)
        serverBansCount--;
    else if (index < sizeof (serverBans) / sizeof (*serverBans) - 1) {
        memmove(serverBans + index, serverBans + index + 1, (serverBansCount - index - 1) * sizeof (*serverBans));
        serverBansCount--;
    } else
        return qtrue;

    return qfalse;
}

/*
==================
SV_ParseCIDRNotation

Parse a CIDR notation type string and return a netadr_t and suffix by reference
==================
 */

static qboolean SV_ParseCIDRNotation(netadr_t *dest, int *mask, char *adrstr) {
    char *suffix;

    suffix = strchr(adrstr, '/');
    if (suffix) {
        *suffix = '\0';
        suffix++;
    }

    if (!NET_StringToAdr(adrstr, dest, NA_UNSPEC))
        return qtrue;

    if (suffix) {
        *mask = atoi(suffix);

        if (dest->type == NA_IP) {
            if (*mask < 1 || *mask > 32)
                *mask = 32;
        } else {
            if (*mask < 1 || *mask > 128)
                *mask = 128;
        }
    } else if (dest->type == NA_IP)
        *mask = 32;
    else
        *mask = 128;

    return qfalse;
}

/*
==================
SV_AddBanToList

Ban a user from being able to play on this server based on his ip address.
==================
 */

static void SV_AddBanToList(qboolean isexception) {
    char *banstring;
    char addy2[NET_ADDRSTRMAXLEN];
    netadr_t ip;
    int index, argc, mask;
    serverBan_t *curban;

    argc = Cmd_Argc();

    if (argc < 2 || argc > 3) {
        Com_Printf("Usage: %s (ip[/subnet] | clientnum [subnet])\n", Cmd_Argv(0));
        return;
    }

    if (serverBansCount > sizeof (serverBans) / sizeof (*serverBans)) {
        Com_Printf("Error: Maximum number of bans/exceptions exceeded.\n");
        return;
    }

    banstring = Cmd_Argv(1);

    if (strchr(banstring, '.') || strchr(banstring, ':')) {
        // This is an ip address, not a client num.

        if (SV_ParseCIDRNotation(&ip, &mask, banstring)) {
            Com_Printf("Error: Invalid address %s\n", banstring);
            return;
        }
    } else {
        client_t *cl;

        // client num.
        if (!com_sv_running->integer) {
            Com_Printf("Server is not running.\n");
            return;
        }

        cl = SV_GetPlayerByNum();

        if (!cl) {
            Com_Printf("Error: Playernum %s does not exist.\n", Cmd_Argv(1));
            return;
        }

        ip = cl->netchan.remoteAddress;

        if (argc == 3) {
            mask = atoi(Cmd_Argv(2));

            if (ip.type == NA_IP) {
                if (mask < 1 || mask > 32)
                    mask = 32;
            } else {
                if (mask < 1 || mask > 128)
                    mask = 128;
            }
        } else
            mask = (ip.type == NA_IP6) ? 128 : 32;
    }

    if (ip.type != NA_IP && ip.type != NA_IP6) {
        Com_Printf("Error: Can ban players connected via the internet only.\n");
        return;
    }

    // first check whether a conflicting ban exists that would supersede the new one.
    for (index = 0; index < serverBansCount; index++) {
        curban = &serverBans[index];

        if (curban->subnet <= mask) {
            if ((curban->isexception || !isexception) && NET_CompareBaseAdrMask(curban->ip, ip, curban->subnet)) {
                Q_strncpyz(addy2, NET_AdrToString(ip), sizeof (addy2));

                Com_Printf("Error: %s %s/%d supersedes %s %s/%d\n", curban->isexception ? "Exception" : "Ban",
                        NET_AdrToString(curban->ip), curban->subnet,
                        isexception ? "exception" : "ban", addy2, mask);
                return;
            }
        }
        if (curban->subnet >= mask) {
            if (!curban->isexception && isexception && NET_CompareBaseAdrMask(curban->ip, ip, mask)) {
                Q_strncpyz(addy2, NET_AdrToString(curban->ip), sizeof (addy2));

                Com_Printf("Error: %s %s/%d supersedes already existing %s %s/%d\n", isexception ? "Exception" : "Ban",
                        NET_AdrToString(ip), mask,
                        curban->isexception ? "exception" : "ban", addy2, curban->subnet);
                return;
            }
        }
    }

    // now delete bans that are superseded by the new one
    index = 0;
    while (index < serverBansCount) {
        curban = &serverBans[index];

        if (curban->subnet > mask && (!curban->isexception || isexception) && NET_CompareBaseAdrMask(curban->ip, ip, mask))
            SV_DelBanEntryFromList(index);
        else
            index++;
    }

    serverBans[serverBansCount].ip = ip;
    serverBans[serverBansCount].subnet = mask;
    serverBans[serverBansCount].isexception = isexception;

    serverBansCount++;

    SV_WriteBans();

    Com_Printf("Added %s: %s/%d\n", isexception ? "ban exception" : "ban",
            NET_AdrToString(ip), mask);
}

/*
==================
SV_DelBanFromList

Remove a ban or an exception from the list.
==================
 */

static void SV_DelBanFromList(qboolean isexception) {
    int index, count = 0, todel, mask;
    netadr_t ip;
    char *banstring;

    if (Cmd_Argc() != 2) {
        Com_Printf("Usage: %s (ip[/subnet] | num)\n", Cmd_Argv(0));
        return;
    }

    banstring = Cmd_Argv(1);

    if (strchr(banstring, '.') || strchr(banstring, ':')) {
        serverBan_t *curban;

        if (SV_ParseCIDRNotation(&ip, &mask, banstring)) {
            Com_Printf("Error: Invalid address %s\n", banstring);
            return;
        }

        index = 0;

        while (index < serverBansCount) {
            curban = &serverBans[index];

            if (curban->isexception == isexception &&
                    curban->subnet >= mask &&
                    NET_CompareBaseAdrMask(curban->ip, ip, mask)) {
                Com_Printf("Deleting %s %s/%d\n",
                        isexception ? "exception" : "ban",
                        NET_AdrToString(curban->ip), curban->subnet);

                SV_DelBanEntryFromList(index);
            } else
                index++;
        }
    } else {
        todel = atoi(Cmd_Argv(1));

        if (todel < 1 || todel > serverBansCount) {
            Com_Printf("Error: Invalid ban number given\n");
            return;
        }

        for (index = 0; index < serverBansCount; index++) {
            if (serverBans[index].isexception == isexception) {
                count++;

                if (count == todel) {
                    Com_Printf("Deleting %s %s/%d\n",
                            isexception ? "exception" : "ban",
                            NET_AdrToString(serverBans[index].ip), serverBans[index].subnet);

                    SV_DelBanEntryFromList(index);

                    break;
                }
            }
        }
    }

    SV_WriteBans();
}

/*
==================
SV_ListBans_f

List all bans and exceptions on console
==================
 */

static void SV_ListBans_f(void) {
    int index, count;
    serverBan_t *ban;

    // List all bans
    for (index = count = 0; index < serverBansCount; index++) {
        ban = &serverBans[index];
        if (!ban->isexception) {
            count++;

            Com_Printf("Ban #%d: %s/%d\n", count,
                    NET_AdrToString(ban->ip), ban->subnet);
        }
    }
    // List all exceptions
    for (index = count = 0; index < serverBansCount; index++) {
        ban = &serverBans[index];
        if (ban->isexception) {
            count++;

            Com_Printf("Except #%d: %s/%d\n", count,
                    NET_AdrToString(ban->ip), ban->subnet);
        }
    }
}

/*
==================
SV_FlushBans_f

Delete all bans and exceptions.
==================
 */

static void SV_FlushBans_f(void) {
    serverBansCount = 0;

    // empty the ban file.
    SV_WriteBans();

    Com_Printf("All bans and exceptions have been deleted.\n");
}

static void SV_BanAddr_f(void) {
    SV_AddBanToList(qfalse);
}

static void SV_ExceptAddr_f(void) {
    SV_AddBanToList(qtrue);
}

static void SV_BanDel_f(void) {
    SV_DelBanFromList(qfalse);
}

static void SV_ExceptDel_f(void) {
    SV_DelBanFromList(qtrue);
}

/*
==================
SV_KickNum_f

Kick a user off of the server  FIXME: move to game
==================
 */
static void SV_KickNum_f(void) {
    client_t *cl;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() != 2) {
        Com_Printf("Usage: kicknum <client number>\n");
        return;
    }

    cl = SV_GetPlayerByNum();
    if (!cl) {
        return;
    }
    if (cl->netchan.remoteAddress.type == NA_LOOPBACK) {
        SV_SendServerCommand(NULL, "print \"%s\"", "Cannot kick host player\n");
        return;
    }

    SV_DropClient(cl, "was kicked");
    cl->lastPacketTime = svs.time; // in case there is a funny zombie
}

/*
================
SV_Status_f
================
 */
static void SV_Status_f(void) {
    int i, j, l;
    client_t *cl;
    playerState_t *ps;
    const char *s;
    int ping;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    Com_Printf("map: %s\n", sv_mapname->string);

    Com_Printf("num score ping name            lastmsg address               qport rate\n");
    Com_Printf("--- ----- ---- --------------- ------- --------------------- ----- -----\n");
    for (i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++) {
        if (!cl->state)
            continue;
        Com_Printf("%3i ", i);
        ps = SV_GameClientNum(i);
        Com_Printf("%5i ", ps->persistant[PERS_SCORE]);

        if (cl->state == CS_CONNECTED)
            Com_Printf("CNCT ");
        else if (cl->state == CS_ZOMBIE)
            Com_Printf("ZMBI ");
        else {
            ping = cl->ping < 9999 ? cl->ping : 9999;
            Com_Printf("%4i ", ping);
        }

        Com_Printf("%s", cl->name);
        // TTimo adding a ^7 to reset the color
        // NOTE: colored names in status breaks the padding (WONTFIX)
        Com_Printf("^7");
        l = 16 - strlen(cl->name);
        for (j = 0; j < l; j++)
            Com_Printf(" ");

        Com_Printf("%7i ", svs.time - cl->lastPacketTime);

        s = NET_AdrToString(cl->netchan.remoteAddress);
        Com_Printf("%s", s);
        l = 22 - strlen(s);
        for (j = 0; j < l; j++)
            Com_Printf(" ");

        Com_Printf("%5i", cl->netchan.qport);

        Com_Printf(" %5i", cl->rate);

        Com_Printf("\n");
    }
    Com_Printf("\n");
}

/*
==================
SV_ConSay_f
==================
 */
static void SV_ConSay_f(void) {
    char *p;
    char text[1024];

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() < 2) {
        return;
    }

    strcpy(text, "console: ");
    p = Cmd_Args();

    if (*p == '"') {
        p++;
        p[strlen(p) - 1] = 0;
    }

    strcat(text, p);

    SV_SendServerCommand(NULL, "chat \"%s\"", text);
}

/*
==================
SV_Heartbeat_f

Also called by SV_DropClient, SV_DirectConnect, and SV_SpawnServer
==================
 */
void SV_Heartbeat_f(void) {
    svs.nextHeartbeatTime = -9999999;
}

/*
===========
SV_Serverinfo_f

Examine the serverinfo string
===========
 */
static void SV_Serverinfo_f(void) {
    Com_Printf("Server info settings:\n");
    Info_Print(Cvar_InfoString(CVAR_SERVERINFO));
}

/*
===========
SV_Systeminfo_f

Examine or change the serverinfo string
===========
 */
static void SV_Systeminfo_f(void) {
    Com_Printf("System info settings:\n");
    Info_Print(Cvar_InfoString(CVAR_SYSTEMINFO));
}

/*
===========
SV_DumpUser_f

Examine all a users info strings FIXME: move to game
===========
 */
static void SV_DumpUser_f(void) {
    client_t *cl;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() != 2) {
        Com_Printf("Usage: info <userid>\n");
        return;
    }

    cl = SV_GetPlayerByHandle();
    if (!cl) {
        return;
    }

    Com_Printf("userinfo\n");
    Com_Printf("--------\n");
    Info_Print(cl->userinfo);
}

/*
==================
SV_ForceCvar_f_helper

Called internally by SV_ForceCvar_f
==================
 */
static void SV_ForceCvar_f_helper(client_t *cl) {
    char *val;
    int len = -1;

    if (strlen(Cmd_Argv(3)) > 0) {
        val = Info_ValueForKey(cl->userinfo, Cmd_Argv(2));
        if (val[0])
            len = strlen(Cmd_Argv(3)) - strlen(val) + strlen(cl->userinfo);
        else
            len = strlen(Cmd_Argv(2)) + strlen(Cmd_Argv(3)) + 2 + strlen(cl->userinfo);
    }
    if (len >= MAX_INFO_STRING)
        SV_DropClient(cl, "userinfo string length exceeded");
    else {
        // In the case where Cmd_Argv(3) is "", the Info_SetValueForKey() call will
        // actually just call Info_RemoveKey().
        Info_SetValueForKey(cl->userinfo, Cmd_Argv(2), Cmd_Argv(3));
        SV_UserinfoChanged(cl);
        // call prog code to allow overrides
        VM_Call(gvm, GAME_CLIENT_USERINFO_CHANGED, cl - svs.clients);
    }
}

/*
==================
SV_ForceCvar_f

Set a cvar for a user
==================
 */
static void SV_ForceCvar_f(void) {
    client_t *cl;
    int i;

    // make sure server is running
    if (!com_sv_running->integer) {
        Com_Printf("Server is not running.\n");
        return;
    }

    if (Cmd_Argc() != 4 || strlen(Cmd_Argv(2)) == 0) {
        Com_Printf("Usage: forcecvar <player name> <cvar name> <cvar value>\nPlayer may be 'allbots'\n");
        return;
    }

    cl = SV_GetPlayerByHandle();
    if (!cl) {
        if (!Q_stricmp(Cmd_Argv(1), "allbots")) {
            for (i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++) {
                if (!cl->state) {
                    continue;
                }
                if (cl->netchan.remoteAddress.type != NA_BOT) {
                    continue;
                }
                SV_ForceCvar_f_helper(cl);
            }
        }
        return;
    }

    SV_ForceCvar_f_helper(cl);
}

/*
=================
SV_KillServer
=================
 */
static void SV_KillServer_f(void) {
    SV_Shutdown("killserver");
}

//===========================================================

/*
==================
SV_CompleteMapName
==================
 */
static void SV_CompleteMapName(char *args, int argNum) {
    if (argNum == 2) {
        Field_CompleteFilename("maps", "bsp", qtrue);
    }
}

#ifdef USE_RUBY
/*
=================
SV_Ruby
=================
 */
void SV_Ruby_f(void) {
  int state;
  if (Cmd_Argc() != 2) {
    Com_Printf("usage: ruby \"code_to_execute\"\nPlease note the \"\", also use single-quotes for ruby strings\n");
    return;
  }
  rb_eval_string_protect(Cmd_Argv(1), &state);
  if (state != 0) {
      Com_Printf("^1ruby: error: %i\nYour syntax may be invalid, or you are referencing a non-existing class/variable.\n", state);
  }
}
#endif

/*
==================
SV_AddOperatorCommands
==================
 */
void SV_AddOperatorCommands(void) {
    static qboolean initialized;

    if (initialized) {
        return;
    }
    initialized = qtrue;

    Cmd_AddCommand("heartbeat", SV_Heartbeat_f);
    Cmd_AddCommand("kick", SV_Kick_f);
#ifndef STANDALONE
    if (!Cvar_VariableIntegerValue("com_standalone")) {
        Cmd_AddCommand("banUser", SV_Ban_f);
        Cmd_AddCommand("banClient", SV_BanNum_f);
    }
#endif
    Cmd_AddCommand("clientkick", SV_KickNum_f);
    Cmd_AddCommand("status", SV_Status_f);
    Cmd_AddCommand("serverinfo", SV_Serverinfo_f);
    Cmd_AddCommand("systeminfo", SV_Systeminfo_f);
    Cmd_AddCommand("dumpuser", SV_DumpUser_f);
    Cmd_AddCommand("map_restart", SV_MapRestart_f);
    Cmd_AddCommand("sectorlist", SV_SectorList_f);
    Cmd_AddCommand("map", SV_Map_f);
    Cmd_SetCommandCompletionFunc("map", SV_CompleteMapName);
#ifndef PRE_RELEASE_DEMO
    Cmd_AddCommand("devmap", SV_Map_f);
    Cmd_SetCommandCompletionFunc("devmap", SV_CompleteMapName);
    Cmd_AddCommand("spmap", SV_Map_f);
    Cmd_SetCommandCompletionFunc("spmap", SV_CompleteMapName);
    Cmd_AddCommand("spdevmap", SV_Map_f);
    Cmd_SetCommandCompletionFunc("spdevmap", SV_CompleteMapName);
#endif
    Cmd_AddCommand("killserver", SV_KillServer_f);
    if (com_dedicated->integer) {
        Cmd_AddCommand("say", SV_ConSay_f);
    }

    Cmd_AddCommand("rehashbans", SV_RehashBans_f);
    Cmd_AddCommand("listbans", SV_ListBans_f);
    Cmd_AddCommand("banaddr", SV_BanAddr_f);
    Cmd_AddCommand("exceptaddr", SV_ExceptAddr_f);
    Cmd_AddCommand("bandel", SV_BanDel_f);
    Cmd_AddCommand("exceptdel", SV_ExceptDel_f);
    Cmd_AddCommand("flushbans", SV_FlushBans_f);

    Cmd_AddCommand("forcecvar", SV_ForceCvar_f);
#ifdef USE_RUBY
    Cmd_AddCommand("ruby", SV_Ruby_f);
#endif
}

/*
==================
SV_RemoveOperatorCommands
==================
 */
void SV_RemoveOperatorCommands(void) {
#if 0
    // removing these won't let the server start again
    Cmd_RemoveCommand("heartbeat");
    Cmd_RemoveCommand("kick");
    Cmd_RemoveCommand("banUser");
    Cmd_RemoveCommand("banClient");
    Cmd_RemoveCommand("status");
    Cmd_RemoveCommand("serverinfo");
    Cmd_RemoveCommand("systeminfo");
    Cmd_RemoveCommand("dumpuser");
    Cmd_RemoveCommand("map_restart");
    Cmd_RemoveCommand("sectorlist");
    Cmd_RemoveCommand("say");
#endif
#ifdef USE_RUBY
  Cmd_RemoveCommand("ruby");
#endif
}

